// Licensed under Apache License version 2.0
package javax.jmdns.impl;

import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.jmdns.impl.constants.DNSState;
import javax.jmdns.impl.tasks.DNSTask;

/**
 * Sets of methods to manage the state machine.<br/>
 * <b>Implementation note:</b> This interface is accessed from multiple threads. The implementation must be thread safe.
 * @author Pierre Frisch
 */
public interface DNSStatefulObject {

   /**
    * This class define a semaphore. On this multiple threads can wait the arrival of one event. Thread wait for a
    * maximum defined by the timeout.
    * <p>
    * Implementation note: this class is based on {@link java.util.concurrent.Semaphore} so that they can be released by
    * the timeout timer.
    * </p>
    * @author Pierre Frisch
    */
   public static final class DNSStatefulObjectSemaphore {
      private static Logger logger = Logger.getLogger(DNSStatefulObjectSemaphore.class.getName());

      private final String _name;

      private final ConcurrentMap<Thread, Semaphore> _semaphores;

      /**
       * @param name Semaphore name for debugging purposes.
       */
      public DNSStatefulObjectSemaphore(String name) {
         super();
         _name = name;
         _semaphores = new ConcurrentHashMap<Thread, Semaphore>(50);
      }

      /**
       * Blocks the current thread until the event arrives or the timeout expires.
       * @param timeout wait period for the event
       */
      public void waitForEvent(long timeout) {
         Thread thread = Thread.currentThread();
         Semaphore semaphore = _semaphores.get(thread);
         if (semaphore == null) {
            semaphore = new Semaphore(1, true);
            semaphore.drainPermits();
            _semaphores.putIfAbsent(thread, semaphore);
         }
         semaphore = _semaphores.get(thread);
         try {
            semaphore.tryAcquire(timeout, TimeUnit.MILLISECONDS);
         } catch (InterruptedException exception) {
            logger.log(Level.FINER, "Exception ", exception);
         }
      }

      /**
       * Signals the semaphore when the event arrives.
       */
      public void signalEvent() {
         Collection<Semaphore> semaphores = _semaphores.values();
         for (Semaphore semaphore : semaphores) {
            semaphore.release();
            semaphores.remove(semaphore);
         }
      }

      @Override
      public String toString() {
         StringBuilder aLog = new StringBuilder(1000);
         aLog.append("Semaphore: ");
         aLog.append(this._name);
         if (_semaphores.size() == 0) {
            aLog.append(" no semaphores.");
         } else {
            aLog.append(" semaphores:\n");
            for (Thread thread : _semaphores.keySet()) {
               aLog.append("\tThread: ");
               aLog.append(thread.getName());
               aLog.append(' ');
               aLog.append(_semaphores.get(thread));
               aLog.append('\n');
            }
         }
         return aLog.toString();
      }

   }

   public static class DefaultImplementation extends ReentrantLock implements DNSStatefulObject {
      private static Logger logger = Logger.getLogger(DefaultImplementation.class.getName());

      private static final long serialVersionUID = -3264781576883412227L;

      private volatile JmDNSImpl _dns;

      protected volatile DNSTask _task;

      protected volatile DNSState _state;

      private final DNSStatefulObjectSemaphore _announcing;

      private final DNSStatefulObjectSemaphore _canceling;

      public DefaultImplementation() {
         super();
         _dns = null;
         _task = null;
         _state = DNSState.PROBING_1;
         _announcing = new DNSStatefulObjectSemaphore("Announce");
         _canceling = new DNSStatefulObjectSemaphore("Cancel");
      }

      /**
       * {@inheritDoc}
       */

      public JmDNSImpl getDns() {
         return this._dns;
      }

      protected void setDns(JmDNSImpl dns) {
         this._dns = dns;
      }

      /**
       * {@inheritDoc}
       */

      public void associateWithTask(DNSTask task, DNSState state) {
         if (this._task == null && this._state == state) {
            this.lock();
            try {
               if (this._task == null && this._state == state) {
                  this.setTask(task);
               }
            } finally {
               this.unlock();
            }
         }
      }

      /**
       * {@inheritDoc}
       */

      public void removeAssociationWithTask(DNSTask task) {
         if (this._task == task) {
            this.lock();
            try {
               if (this._task == task) {
                  this.setTask(null);
               }
            } finally {
               this.unlock();
            }
         }
      }

      /**
       * {@inheritDoc}
       */

      public boolean isAssociatedWithTask(DNSTask task, DNSState state) {
         this.lock();
         try {
            return this._task == task && this._state == state;
         } finally {
            this.unlock();
         }
      }

      protected void setTask(DNSTask task) {
         this._task = task;
      }

      /**
       * @param state the state to set
       */
      protected void setState(DNSState state) {
         this.lock();
         try {
            this._state = state;
            if (this.isAnnounced()) {
               _announcing.signalEvent();
            }
            if (this.isCanceled()) {
               _canceling.signalEvent();
               // clear any waiting announcing
               _announcing.signalEvent();
            }
         } finally {
            this.unlock();
         }
      }

      /**
       * {@inheritDoc}
       */

      public boolean advanceState(DNSTask task) {
         boolean result = true;
         if (this._task == task) {
            this.lock();
            try {
               if (this._task == task) {
                  this.setState(this._state.advance());
               } else {
                  logger.warning("Trying to advance state whhen not the owner. owner: " + this._task + " perpetrator: " + task);
               }
            } finally {
               this.unlock();
            }
         }
         return result;
      }

      /**
       * {@inheritDoc}
       */

      public boolean revertState() {
         boolean result = true;
         if (!this.willCancel()) {
            this.lock();
            try {
               if (!this.willCancel()) {
                  this.setState(this._state.revert());
                  this.setTask(null);
               }
            } finally {
               this.unlock();
            }
         }
         return result;
      }

      /**
       * {@inheritDoc}
       */

      public boolean cancelState() {
         boolean result = false;
         if (!this.willCancel()) {
            this.lock();
            try {
               if (!this.willCancel()) {
                  this.setState(DNSState.CANCELING_1);
                  this.setTask(null);
                  result = true;
               }
            } finally {
               this.unlock();
            }
         }
         return result;
      }

      /**
       * {@inheritDoc}
       */

      public boolean closeState() {
         boolean result = false;
         if (!this.willClose()) {
            this.lock();
            try {
               if (!this.willClose()) {
                  this.setState(DNSState.CLOSING);
                  this.setTask(null);
                  result = true;
               }
            } finally {
               this.unlock();
            }
         }
         return result;
      }

      /**
       * {@inheritDoc}
       */

      public boolean recoverState() {
         boolean result = false;
         this.lock();
         try {
            this.setState(DNSState.PROBING_1);
            this.setTask(null);
         } finally {
            this.unlock();
         }
         return result;
      }

      /**
       * {@inheritDoc}
       */

      public boolean isProbing() {
         return this._state.isProbing();
      }

      /**
       * {@inheritDoc}
       */

      public boolean isAnnouncing() {
         return this._state.isAnnouncing();
      }

      /**
       * {@inheritDoc}
       */

      public boolean isAnnounced() {
         return this._state.isAnnounced();
      }

      /**
       * {@inheritDoc}
       */

      public boolean isCanceling() {
         return this._state.isCanceling();
      }

      /**
       * {@inheritDoc}
       */

      public boolean isCanceled() {
         return this._state.isCanceled();
      }

      /**
       * {@inheritDoc}
       */

      public boolean isClosing() {
         return this._state.isClosing();
      }

      /**
       * {@inheritDoc}
       */

      public boolean isClosed() {
         return this._state.isClosed();
      }

      private boolean willCancel() {
         return this._state.isCanceled() || this._state.isCanceling();
      }

      private boolean willClose() {
         return this._state.isClosed() || this._state.isClosing();
      }

      /**
       * {@inheritDoc}
       */

      public boolean waitForAnnounced(long timeout) {
         if (!this.isAnnounced() && !this.willCancel()) {
            _announcing.waitForEvent(timeout);
         }
         if (!this.isAnnounced()) {
            if (this.willCancel() || this.willClose()) {
               logger.fine("Wait for announced cancelled: " + this);
            } else {
               logger.warning("Wait for announced timed out: " + this);
            }
         }
         return this.isAnnounced();
      }

      /**
       * {@inheritDoc}
       */

      public boolean waitForCanceled(long timeout) {
         if (!this.isCanceled()) {
            _canceling.waitForEvent(timeout);
         }
         if (!this.isCanceled() && !this.willClose()) {
            logger.warning("Wait for canceled timed out: " + this);
         }
         return this.isCanceled();
      }

      /**
       * {@inheritDoc}
       */

      @Override
      public String toString() {
         return (_dns != null ? "DNS: " + _dns.getName() : "NO DNS") + " state: " + _state + " task: " + _task;
      }

   }

   /**
    * Returns the DNS associated with this object.
    * @return DNS resolver
    */
   public JmDNSImpl getDns();

   /**
    * Sets the task associated with this Object.
    * @param task associated task
    * @param state state of the task
    */
   public void associateWithTask(DNSTask task, DNSState state);

   /**
    * Remove the association of the task with this Object.
    * @param task associated task
    */
   public void removeAssociationWithTask(DNSTask task);

   /**
    * Checks if this object is associated with the task and in the same state.
    * @param task associated task
    * @param state state of the task
    * @return <code>true</code> is the task is associated with this object, <code>false</code> otherwise.
    */
   public boolean isAssociatedWithTask(DNSTask task, DNSState state);

   /**
    * Sets the state and notifies all objects that wait on the ServiceInfo.
    * @param task associated task
    * @return <code>true</code if the state was changed by this thread, <code>false</code> otherwise.
    * @see DNSState#advance()
    */
   public boolean advanceState(DNSTask task);

   /**
    * Sets the state and notifies all objects that wait on the ServiceInfo.
    * @return <code>true</code if the state was changed by this thread, <code>false</code> otherwise.
    * @see DNSState#revert()
    */
   public boolean revertState();

   /**
    * Sets the state and notifies all objects that wait on the ServiceInfo.
    * @return <code>true</code if the state was changed by this thread, <code>false</code> otherwise.
    */
   public boolean cancelState();

   /**
    * Sets the state and notifies all objects that wait on the ServiceInfo.
    * @return <code>true</code if the state was changed by this thread, <code>false</code> otherwise.
    */
   public boolean closeState();

   /**
    * Sets the state and notifies all objects that wait on the ServiceInfo.
    * @return <code>true</code if the state was changed by this thread, <code>false</code> otherwise.
    */
   public boolean recoverState();

   /**
    * Returns true, if this is a probing state.
    * @return <code>true</code> if probing state, <code>false</code> otherwise
    */
   public boolean isProbing();

   /**
    * Returns true, if this is an announcing state.
    * @return <code>true</code> if announcing state, <code>false</code> otherwise
    */
   public boolean isAnnouncing();

   /**
    * Returns true, if this is an announced state.
    * @return <code>true</code> if announced state, <code>false</code> otherwise
    */
   public boolean isAnnounced();

   /**
    * Returns true, if this is a canceling state.
    * @return <code>true</code> if canceling state, <code>false</code> otherwise
    */
   public boolean isCanceling();

   /**
    * Returns true, if this is a canceled state.
    * @return <code>true</code> if canceled state, <code>false</code> otherwise
    */
   public boolean isCanceled();

   /**
    * Returns true, if this is a closing state.
    * @return <code>true</code> if closing state, <code>false</code> otherwise
    */
   public boolean isClosing();

   /**
    * Returns true, if this is a closed state.
    * @return <code>true</code> if closed state, <code>false</code> otherwise
    */
   public boolean isClosed();

   /**
    * Waits for the object to be announced.
    * @param timeout the maximum time to wait in milliseconds.
    * @return <code>true</code> if the object is announced, <code>false</code> otherwise
    */
   public boolean waitForAnnounced(long timeout);

   /**
    * Waits for the object to be canceled.
    * @param timeout the maximum time to wait in milliseconds.
    * @return <code>true</code> if the object is canceled, <code>false</code> otherwise
    */
   public boolean waitForCanceled(long timeout);

}
